package tau.pimpmyfreedb.master;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import tau.facebook.FacebookAPI;
import tau.facebook.FacebookFriend;
import tau.freedbimporter.FreeDBDataPimper;
import tau.freedbimporter.FreeDBPackParser;
import tau.freedbimporter.RawFreeDBDiscInfo;
import tau.pimpmyfreedb.GUI.SWTUtil;
import tau.pimpmyfreedb.common.IProgressUpdateClient;
import tau.pimpmyfreedb.common.types.Album;
import tau.pimpmyfreedb.common.types.Artist;
import tau.pimpmyfreedb.common.types.DataItemBase;
import tau.pimpmyfreedb.common.types.EditData;
import tau.pimpmyfreedb.common.types.FBLike;
import tau.pimpmyfreedb.common.types.MusicItem;
import tau.pimpmyfreedb.common.types.PagedSearchResult;
import tau.pimpmyfreedb.common.types.PimpFBFriend;
import tau.pimpmyfreedb.common.types.Track;
import tau.pimpmyfreedb.common.types.Tuple;
import tau.pimpmyfreedb.common.types.User;
import tau.pimpmyfreedb.dal.DALController;
import tau.pimpmyfreedb.dal.DalResponse;
import tau.pimpmyfreedb.dal.DataFilter;
import tau.pimpmyfreedb.dal.DataItemRelation;
import tau.pimpmyfreedb.dal.DataType;
import tau.pimpmyfreedb.dal.IDalController;
import tau.pimpmyfreedb.dal.IDataItem;
import tau.youtube.YouTubeAPI;

/**
 * 
 * The class provides the following functionality:
 * 
 * 1. Wraps calls to external services, and runs them in a background thread.
 * 
 * 2. Provides easy-to-work-with API for those services.
 * 
 * Works against:
 * 
 * 1. Queries against the MySQL database.
 * 
 * 2. Queries against the Facebook servers.
 * 
 * 3. Queries against the YouTube servers.
 * 
 * 4. All class methods are asynchronous (with the exception of connect() and
 * disconnect()).
 * 
 * IMPORTANT NOTE: The first call, before any query to the database is made,
 * must be connect().
 * 
 */
public class UnifiedDalBridge {
    private ExecutorService threadPool;
    private IDalController dal;
    private YouTubeAPI youtube;
    private long closeTimeout;
    private long relevantSearchId;

    public UnifiedDalBridge(int minSize, int maxSize, int idleTime,
	    int queueSize, long closeTimeout) throws ClassNotFoundException {
	this.closeTimeout = closeTimeout;

	// init the dal object
	// this.dal = new DummyDalController();
	this.dal = new DALController();

	// init the thread pool
	threadPool = new ThreadPoolExecutor(minSize, // core (min) num of
		// threads
		maxSize, // max num of threads
		idleTime, // idle time before disposing redundant threads (above
		// the
		// minimum)
		TimeUnit.SECONDS, // idle time units
		new ArrayBlockingQueue<Runnable> // the jobs queue
		(queueSize, // queue size
			true // fair, i.e. FIFO
		));

	this.youtube = new YouTubeAPI();
	this.relevantSearchId = -1;
    }

    // single get

    public void GetYouTubeURL(final String trackName, final String artistName,
	    final YouTubeAPI.PlayerType playerType,
	    final Callback<DalResponse> callback) {
	threadPool.execute(new Runnable() {

	    @Override
	    public void run() {
		try {
		    String url = youtube.GetVideoURL(trackName, artistName,
			    playerType);
		    callback.execute(new DalResponse(true, 0, url));
		} catch (Exception e) {
		    callback.execute(new DalResponse(false, 0, e.getMessage()));
		}
	    }
	});
    }

    // bulk get / search

    private long GetNextSearchId() {
	// relevantSearchId = (relevantSearchId + 1) % Long.MAX_VALUE;
	// return relevantSearchId
	// in case 2^64 searches is a possible use case, uncomment the above

	return ++relevantSearchId;
    }

    public void searchMusic(final DataFilter filter,
	    final GuiCallback<PagedSearchResult<MusicItem[]>> guiCallback) {

	// we don't want to execute obsolete searches for nothing so we assign
	// an identifier to define whether or not this search is still required.
	// we check twice for relevance, once before the query execution and
	// once before the callback execution. this double check is done because
	// a new music search query might have been submitted while the previous
	// query was executed and if so the old query's callback execution is
	// needles.
	final long mySearchId = GetNextSearchId();

	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {

		PagedSearchResult<MusicItem[]> res = new PagedSearchResult<MusicItem[]>(
			new MusicItem[0], 0, 0);

		// execute query
		if (relevantSearchId == mySearchId)
		    try {
			PagedSearchResult<IDataItem[]> items = dal
				.getData(filter);
			res.setTotalResultsCount(items.getTotalResultsCount());
			res.setCurrentResultsPage(items.getCurrentResultsPage());
			res.setResult(Arrays.copyOf(items.getResult(),
				items.getResult().length, MusicItem[].class));
		    } catch (SQLException e) {
			SWTUtil.errorBox(e);
		    }

		// execute the callback
		if (relevantSearchId == mySearchId)
		    guiCallback.execute(res);
	    }
	});
    }

    public void getFriendsByUser(final int userID,
	    final Callback<IDataItem[]> callback) {
	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {

		try {
		    // set the filter
		    DataFilter filter = new DataFilter();
		    filter.setReturnType(DataType.FB_FRIEND);
		    filter.setRestrictByItem(userID, DataType.PIMP_USER);

		    // execute query
		    IDataItem[] res = dal.getData(filter).getResult();

		    // execute the callback
		    callback.execute(res);
		} catch (SQLException e) {
		    SWTUtil.errorBox(e);
		}

	    }
	});
    }

    public void getLikesByFriend(final int friendId,
	    final Callback<IDataItem[]> callback) {
	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {
		try {
		    // set the filter
		    DataFilter filter = new DataFilter();
		    filter.setReturnType(DataType.FB_LIKE);
		    filter.setRestrictByItem(friendId, DataType.FB_FRIEND);

		    // execute query
		    IDataItem[] res = dal.getData(filter).getResult();

		    // execute the callback
		    callback.execute(res);
		} catch (SQLException e) {
		    SWTUtil.errorBox(e);
		}

	    }
	});
    }

    public void getUsersList(final Callback<IDataItem[]> callback) {
	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {
		try {
		    DataFilter filter = new DataFilter();
		    filter.setReturnType(DataType.PIMP_USER);

		    // execute query
		    IDataItem[] res = dal.getData(filter).getResult();

		    // execute the callback
		    callback.execute(res);
		} catch (SQLException e) {
		    SWTUtil.errorBox(e);
		}
	    }
	});
    }

    public void deleteItem(final DataType dataType, final int dataID,
	    final Callback<DalResponse> callback) {
	deleteItems(new IDataItem[] { createTempItem(dataType, dataID) },
		callback);
    }

    private IDataItem createTempItem(final DataType dataType, final int dataID) {
	return new DataItemBase(dataID, "") {
	    @Override
	    public DataType getDataType() {
		return dataType;
	    }
	};
    }

    public void deleteItems(final IDataItem[] items,
	    final Callback<DalResponse> callback) {
	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {
		try {
		    // Delete the data
		    DalResponse res = dal.deleteData(items);
		    // Execute the callback
		    callback.execute(res);
		} catch (SQLException e) {
		    SWTUtil.errorBox(e);
		}
	    }

	});

    }

    // add/edit

    public void importData(final String[] filePaths,
	    final IProgressUpdateClient client,
	    final Callback<DalResponse> callback) throws FileNotFoundException,
	    IOException {

	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {
		boolean succeeded = false;
		String msg = "";
		int rowsAffected = 0;
		try {
		    for (final String filePath : filePaths) {
			FreeDBPackParser parser = null;

			try {
			    parser = new FreeDBPackParser(filePath);
			    msg += importFreeDBFile(filePath, dal, client);
			    succeeded = true;
			} catch (IOException e) {
			    msg += e.getMessage() + "\r\n";
			} catch (ClassNotFoundException e) {
			    msg += e.getMessage() + "\r\n";
			} catch (SQLException e) {
			    msg += e.getMessage() + "\r\n";
			} finally {
			    if (parser != null)
				parser.close();
			}
			if (!succeeded)
			    break;
		    }
		} catch (IOException e) {
		    msg += e.getMessage() + "\r\n";
		} finally {
		    client.handleOperationComplete();
		    callback.execute(new DalResponse(succeeded, rowsAffected,
			    msg.isEmpty() && succeeded ? "Import succeded"
				    : msg));
		}

	    }
	});

    }

    private static String importFreeDBFile(String filePath,
	    IDalController controller, IProgressUpdateClient progress)
	    throws FileNotFoundException, IOException, ClassNotFoundException,
	    SQLException {
	FreeDBPackParser parser = new FreeDBPackParser(filePath);
	FreeDBDataPimper pimper = new FreeDBDataPimper();
	try {
	    ArrayList<RawFreeDBDiscInfo> discs = new ArrayList<RawFreeDBDiscInfo>();
	    int numberEntriesRead = 0, numberArtistsAdded = 0, numberAlbumsAdded = 0, numberTracksAdded = 0;
	    String totals = "Imported 0 entries.";
	    while (parser.readEntries(200, discs, progress)
		    && !progress.isCancelled()) {
		System.out.println("Read entries...");
		FreeDBDataPimper.ConvertedData data = pimper.convertData(discs);
		System.out.println("Converted data...");
		totals = String.format(filePath
			+ "\nFinished importing %d FreeDB entries: "
			+ "Added %d artists, %d albums and %d tracks to DB.\n",
			numberEntriesRead, numberArtistsAdded,
			numberAlbumsAdded, numberTracksAdded);
		String currentStatus = String.format(
			"Current bulk: Now inserting %d artists, "
				+ "%d albums, %d tracks...\n", discs.size(),
			data.getArtists().size(), data.getAlbums().size(), data
				.getTracks().size());
		progress.handleProgressUpdate(-1, totals + currentStatus);
		numberEntriesRead += discs.size();
		try {
		    addConvertedData(controller, data);
		} catch (SQLException e) {
		    e.printStackTrace();
		}
		numberArtistsAdded += data.getArtists().size();
		numberAlbumsAdded += data.getAlbums().size();
		numberTracksAdded += data.getTracks().size();
		System.out.println("Added data to DB.");
	    }
	    if (progress.isCancelled())
		totals = "Operation stoped. " + totals;
	    return totals;
	} finally {
	    parser.close();
	}
    }

    private static void addConvertedData(IDalController controller,
	    FreeDBDataPimper.ConvertedData data) throws SQLException {
	ArrayList<IDataItem> items = new ArrayList<IDataItem>();
	for (Album album : data.getAlbums()) {
	    items.add(album);
	}
	for (Track track : data.getTracks()) {
	    items.add(track);
	}
	for (Artist artist : data.getArtists().values()) {
	    items.add(artist);
	}
	IDataItem[] arr = new IDataItem[0];
	controller.insertData(items.toArray(arr));
	ArrayList<DataItemRelation> relations = new ArrayList<DataItemRelation>();
	for (Tuple<Album, Track> tuple : data.getAlbumTrackRelations()) {
	    relations.add(new DataItemRelation(tuple.getX(), tuple.getY()));
	}
	for (Tuple<Artist, Album> tuple : data.getArtistAlbumRelations()) {
	    relations.add(new DataItemRelation(tuple.getX(), tuple.getY()));
	}
	for (Tuple<Artist, Track> tuple : data.getArtistTrackRelations()) {
	    relations.add(new DataItemRelation(tuple.getX(), tuple.getY()));
	}
	controller.insertRelations(relations);
    }

    public void submitUser(final User user,
	    final IProgressUpdateClient progressClient,
	    final Callback<DalResponse> callback) {
	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {

		// submit the user
		DalResponse response;
		try {
		    // Get data from FaceBook
		    progressClient.handleProgressUpdate(-1,
			    "Getting friends from FaceBook API...");
		    FacebookAPI api = new FacebookAPI(user.getFbToken());
		    FacebookFriend[] friends = api.getFriends();

		    // Submit the user
		    progressClient.handleProgressUpdate(-1,
			    "Adding user to DB...");
		    dal.insertData(new IDataItem[] { user });
		    if (user.getDataId() == IDataItem.NewItemId) {
			throw new Exception("User didn't get ID after submit.");
		    }
		    // Submit the user's friends
		    progressClient.handleProgressUpdate(-1,
			    "Adding friends to DB...");
		    ArrayList<IDataItem> items = new ArrayList<IDataItem>();
		    HashMap<PimpFBFriend, String[]> likesForUser = new HashMap<PimpFBFriend, String[]>();
		    for (FacebookFriend fbFriend : friends) {

			String name = fbFriend.getFirstName() + " "
				+ fbFriend.getLastName();
			if (FreeDBPackParser.isPureAscii(name)) {
			    PimpFBFriend pimpFriend = new PimpFBFriend(
				    IDataItem.NewItemId, name, user.getDataId());
			    likesForUser.put(pimpFriend,
				    fbFriend.getMusicLikes());
			    items.add(pimpFriend);
			}
		    }
		    dal.insertData(items.toArray(new IDataItem[0]));
		    ArrayList<FBLike> allLikes = new ArrayList<FBLike>();
		    for (PimpFBFriend addedItem : likesForUser.keySet()) {
			String[] likes = likesForUser.get(addedItem);
			for (String musicLike : likes) {
			    if (!FreeDBPackParser.isPureAscii(musicLike))
				continue;
			    FBLike like = new FBLike(IDataItem.NewItemId,
				    musicLike, addedItem.getDataId());
			    allLikes.add(like);
			}
		    }
		    progressClient.handleProgressUpdate(-1,
			    "Adding friends' music likes to DB...");
		    dal.insertData(allLikes.toArray(new IDataItem[0]));

		    System.out.println("Done.");
		    response = new DalResponse(true, items.size(), "Done");
		} catch (SQLException e) {
		    response = new DalResponse(false, 0, e.getMessage());
		} catch (Exception e) {
		    response = new DalResponse(false, 0, e.getMessage());
		}
		// execute callback
		callback.execute(response);
	    }
	});
    }

    // Connect / Disconnect

    public void Connect(final String url, final String username,
	    final String password, final Callback<DalResponse> callback) {
	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {
		try {
		    dal.connect(url, username, password);
		    callback.execute(new DalResponse(true, 0,
			    "Connected Successfuly"));
		} catch (SQLException e) {
		    callback.execute(new DalResponse(false, 0, e.getMessage()));
		}
	    }
	});
    }

    public void Disconnect() {
	try {
	    // terminate remaining jobs
	    threadPool.shutdown();
	    threadPool.awaitTermination(closeTimeout, TimeUnit.SECONDS);
	    threadPool.shutdownNow();

	    // close db connection
	    dal.disconnect();
	} catch (InterruptedException e) {
	    SWTUtil.errorBox(e);
	} catch (SQLException e) {
	    SWTUtil.errorBox(e);
	}
    }

    public boolean isConnected() {
	return dal.isConnected();
    }

    public void updateDataFromGui(final EditData editData,
	    final Callback<DalResponse> callback) {

	threadPool.execute(new Runnable() {
	    @Override
	    public void run() {
		try {
		    DalResponse res = null;
		    // Update
		    if (editData.getMusicItemToUpdate() != null) {
		    MusicItem[] temp = { editData.getMusicItemToUpdate() };
		    res = dal.updateData(temp);
		    }
		    // Create
		    res = dal.insertData(editData.getItemsToCreate().toArray(
			    new IDataItem[0]));
		    ArrayList<Tuple<MusicItem, MusicItem>> newData = editData
			    .getRelations();
		    ArrayList<Tuple<MusicItem, MusicItem>> oldData = editData
			    .getOldRelations();

		    // Manage Relations
		    for (int i = 0; i <= newData.size() - 1; i++) {
			if (oldData.contains(newData.get(i))) {
			    oldData.remove(newData.get(i));
			    newData.remove(i);
			    i--;
			}
		    }

		    ArrayList<DataItemRelation> addRelations = new ArrayList<DataItemRelation>();
		    ArrayList<DataItemRelation> removeRelations = new ArrayList<DataItemRelation>();
		    ArrayList<MusicItem> tracksToRemove = new ArrayList<MusicItem>();

		    // Arrange relation to add
		    for (Tuple<MusicItem, MusicItem> tuple : newData) {
			addRelations.add(new DataItemRelation(tuple.getX(),
				tuple.getY()));
		    }
		    res = dal.insertRelations(addRelations);

		    // Arrange relations to remove
		    // Tracks are removed, others just lose relations
		    for (Tuple<MusicItem, MusicItem> tuple : oldData) {
			if (tuple.getX().getDataType() == DataType.TRACK) {
			    tracksToRemove.add(tuple.getX());
			} else if (tuple.getY().getDataType() == DataType.TRACK) {
			    tracksToRemove.add(tuple.getY());
			} else {
			    removeRelations.add(new DataItemRelation(tuple
				    .getX(), tuple.getY()));
			}
		    }
		    res = dal.deleteData(tracksToRemove
			    .toArray(new IDataItem[0]));
		    res = dal.deleteRelations(removeRelations);

		    callback.execute(res);
		} catch (SQLException e) {
		    e.printStackTrace();
		    callback.execute(new DalResponse(false, 0, e.getMessage()));
		}

	    }
	});
    }

}
